package org.groovyflow.util

import static org.groovyflow.util.Decision.*

/**
 * @author Chuck Tassoni
 */
class DecisionTest extends GroovyTestCase {

  //@Typed
  public void testTypicalUsage() {
    def repository = new StudentRepository() {

      Map data = ['math': [new Student(grade: 70), new Student(grade: 69, name: "Jones"), new Student(grade: 40, name: "Smith"), new Student(grade: 96, name: "Williams")],
              "homeroom": [new Student(grade: 70, name: "Snodgrass"), new Student(grade: 40, name: "Hamilton")]
      ]

      List<Student> getStudentsForCourse(String course) {
        data[course]
      }
    }
    def failedStudentRegistry = new HashSet()
    def honorStudentRegistry = new HashSet()
    def suspendedStudentRegistry = new HashSet()

    def mathRules = new Decision().if {Student student -> student.grade < 70}.do {student -> failedStudentRegistry << student}
    .if {Student student -> student.grade >= 95}.do {student -> honorStudentRegistry << student}
    .safe()
    def studentHandler = new StudentHandler(repository: repository, rules: mathRules)
    studentHandler.handle("math")

    def homeroomRules = new Decision().if {Student student -> student.grade <= 40}.do {student -> suspendedStudentRegistry << student}.safe()
    studentHandler.rules = homeroomRules
    studentHandler.handle("homeroom")

    assert 2 == failedStudentRegistry.size()
    assert 1 == honorStudentRegistry.size()
    assert 1 == suspendedStudentRegistry.size()
  }


  public void testRulesFromText() {
    Closure collectsTestResults = {it.name}

    //Create rules from text
    List categoryRulesFromSomeRepository = ["30m < revenue and revenue <= 100m and 10m < profits and profits <= 20m",
            "revenue > 100m and profits > 20m",
            "profits < 0m"]
    List categories = ["promising", "outstanding", "risky"]
    Decision decision = rulesFromText(categoryRulesFromSomeRepository, categories)

    //Apply the rules
    MultiMap categoryResults = new MultiMap()
    List<Company> data = [new Company(profits: -10, revenue: 0, name: "risky"),
            new Company(profits: 10000001, revenue: 30000001, name: "promising1"),
            new Company(profits: 10000002, revenue: 30000002, name: "promising2"),
            new Company(profits: 100000020, revenue: 300000020, name: "outstanding"),
            new Company(profits: 1, revenue: 1, name: "uncategorized")]
    applyRules(decision, categoryResults, data)

    //Verify that the rules executed the expected categorization
    assert 3 == categoryResults.size()  //Found 3 categories-- a category may contain more than one element
    assert ["risky"] == categoryResults["risky"].collect(collectsTestResults)
    assert ["promising1", "promising2"] == categoryResults["promising"].collect(collectsTestResults)
    assert ["outstanding"] == categoryResults["outstanding"].collect(collectsTestResults)

    //Apply the same rules to new data
    MultiMap moreResults = new MultiMap()
    List<Company> moreData = [new Company(profits: 100000020, revenue: 300000020, name: "outstanding1"),
            new Company(profits: 100000020, revenue: 300000020, name: "outstanding2"),
            new Company(profits: 1, revenue: 1, name: "uncategorized")]
    applyRules(decision, moreResults, moreData)

    assert ["outstanding1", "outstanding2"] == moreResults["outstanding"].collect(collectsTestResults)
  }

  /**
   * Turns text into Closure predicates and actions that are used as rules.
   * Note that we don't need to recreate rules because we don't depend on Groovy Binding to
   * insert new data into the rules.  Instead the rules are formed with the expectation that
   * the appropriate invocation context (known in Groovy as the 'delegate') will be  inserted
   * when the applyRules method is called.
   */
  public Decision rulesFromText(List<String> categoryRules, List<String> categories) {
    GroovyShell shell = new GroovyShell()
    Decision decision = new Decision().safe()
    categoryRules.eachWithIndex { String text, int index ->
      text = text.replace("m", "000000").replace("and", "&&")
      //If the text rule is true, return the delegate, which will be set in applyRules()
      text = """{-> if($text) delegate }"""
      Closure predicate = (Closure) shell.evaluate(text)
      //This line anticipates that the map that gathers results will be set as the invocation context
      //(i.e. the delegate) in the applyRules method below.
      Closure action = {Company company -> delegate[categories[index]] = company}
      decision.if(predicate).do(action)
    }
    decision
  }

  def applyRules(Decision decision, MultiMap categoryResults, List data) {
    data.each { Company company ->
      //Set the appropriate invocation contexts for the rules
      decision.setPredicateDelegate(company).setActionDelegate(categoryResults).apply()
    }
  }

    //As of this writing, p. 10 of http://www.scala-lang.org/docu/files/ScalaTutorial.pdf has an example that
    //this test emulates
  public void testTreeScalaTutorialExample() {
    def expression = new Sum(new Sum(new Var("x"), new Var("x")), new Sum(new Constant(7), new Var("y")))
    assert 24 == eval(expression, makeEnv())
  }

  public void testTreeScalaTutorialExampleWithDifferentSyntax() {
    def expression = new Sum(new Sum(new Var("x"), new Var("x")), new Sum(new Constant(7), new Var("y")))
    assert 24 == anotherEval(expression, makeEnv())
  }

  public void testTreeScalaTutorialExampleWhereRulesDoNotUseClosureScopedVariables() {
    def expression = new Sum(new Sum(new Var("x"), new Var("x")), new Sum(new Constant(7), new Var("y")))
    assert 24 == evalWhereRulesDoNotUseClosureScopedVariables(expression, makeEnv())
  }

    /*Compare to http://www.scala-lang.org/docu/files/ScalaTutorial.pdf page 10
        def eval(t: Tree, env: Environment): Int = t match {
          case Sum(l, r) => eval(l, env) + eval(r, env)
          case Var(n) => env(n)
          case Const(v) => v
        }
    */
  def eval(tree, env) {
    new Decision().apply(
            rule(toIf({tree instanceof Sum}, {[tree.left, tree.right]}), { left, right -> eval(left, env) + eval(right, env) }),
            rule({tree instanceof Var }, { env(tree) }),
            rule({tree instanceof Constant }, {tree.v})
    )
  }

  def anotherEval(tree, env) {
    new Decision().if {rule({tree instanceof Sum}, {[tree.left, tree.right]}).eval()}
            .do { left, right -> anotherEval(left, env) + anotherEval(right, env) }
            .if {rule({tree instanceof Var}, {(Var) tree}).eval()}.do { var -> env(var) }
            .if {rule({tree instanceof Constant}, {tree.v}).eval()}.do {it}
            .apply()
  }


  def evalWhereRulesDoNotUseClosureScopedVariables(def treeNotDirect, def envNotDirect) {
      //Note that 1) here rules do not refer to method parameters, and 2) rules need only declare the parameters
      //they require (e.g. some rules only require a 'tree' parameter, some require  more parameters.
      //Note especially the action in the first rule below.  It accepts the arguments passed into the withArgs method,
      //and also accepts the two arguments created by that action's predicate.  Further, note that the predicate had
      //to put those two arguments into a list, but the Decision took them out of the list and applied them as
      //individual arguments to the action.  (If the predicate had only wished to return one value, it
      //wouldn't have had to put it in a list-- the Decision would have figured out the right thing to do in that case.
      new Decision().withArgs([treeNotDirect, envNotDirect]).apply(
              //We use toIfResult here because we want to pass a result, not a Closure, to the action. In contrast, we used toIf() in the
              //example above because in that situation we needed code that evaluated to a Closure, not to the result of a Closure.
              rule({tree -> toIfResult({ tree instanceof Sum}, {[tree.left, tree.right]}) }, { tree, env, left, right -> evalWhereRulesDoNotUseClosureScopedVariables(left, env) + evalWhereRulesDoNotUseClosureScopedVariables(right, env)}),
              rule({tree -> tree instanceof Var }, { tree, env -> env(tree) }),
              rule({tree -> tree instanceof Constant }, {tree -> tree.v})
      )
    }

  def makeEnv() {
    //Create the environment with a Decision instead of a map, just to show the scope of the
    //construct.
    {  Var var ->
      new Decision().apply(
              rule({var.name == "x"}, {5}),
              rule({var.name == "y"}, {7})
      )
    }
  }

  def makeConstant() {
    new Constant(3)
  }

    //@Typed
  public void testPredicateReturningScalarValueDoesNotNeedToPutItInAList() {
    def expected = "hello"
    assert expected == new Decision().apply(
            rule({expected}, {it})
    )
  }

  public void testTheUseOfSimpleRuleObjectsThatDoNotDependOnClosureScopedVariable() {
    String messagePassedFromPredictateToAction = "message"
    def argsToPassToPredicatesAndActions = [1, 2]
    //We don't need to encase the action's return in a list when the return is a single scalar value, but
    // let's check that things work ok when we choose to do that.
    def closureUsedByPredicateThatServesToSendAdditionalArgToPredicate = {[messagePassedFromPredictateToAction]}
    assert 2 + messagePassedFromPredictateToAction ==
            simpleRuleObjectsThatDoNotDependOnClosureScopedVariablesImpl(argsToPassToPredicatesAndActions,
                    closureUsedByPredicateThatServesToSendAdditionalArgToPredicate)
  }

  public void testTheUseOfRuleObjectsThatDoNotDependOnClosureScopedVariablesAndPredicatedDoesNotBotherWithListReturn() {
    def argsToPassToPredicatesAndActions = [1, 2]
    String messagePassedFromPredictateToAction = "message"

    //This time we don't bother with putting the returned element ijn a list, knowing that the Decision will handle that.
    def closureUsedByPredicateThatServesToSendAdditionalArgToPredicate = {messagePassedFromPredictateToAction}
    assert 2 + messagePassedFromPredictateToAction ==
            simpleRuleObjectsThatDoNotDependOnClosureScopedVariablesImpl(argsToPassToPredicatesAndActions,
                    closureUsedByPredicateThatServesToSendAdditionalArgToPredicate)
  }


  def simpleRuleObjectsThatDoNotDependOnClosureScopedVariablesImpl(def args, def rulePredMessageReturner) {
    //Note that rulePredMessageReturner returns a value that is available to the action after all the
    //'args values have been declared.  This value is named 'messageFromPred' below.
    new Decision().withArgs(args).apply(
            new Rule(
                    //Returning a scalar value, not a list, is ok here.
                    {x, y ->  x > 0 || y > 0 ? rulePredMessageReturner() : false},
                    {x, y, String messageFromPred -> x * y + messageFromPred}
            )
    )
  }

  public void testTheUseOfArgForRulesThatDoNotDependOnClosureScopeWhenTheArgIsAScalar() {
    String messagePassedFromPredictateToAction = "message"
    //Another way of calling the withArgs method:
    //passing a different arg to predicate than to action, and not putting predicate or
    //action args into a list, since they're both scalar values.
    assert 2 + messagePassedFromPredictateToAction == new Decision().withArgs(1, 2).apply(
            new Rule(
                    {x ->  x > 0 ? messagePassedFromPredictateToAction : false},
                    { y, message ->  y + message}
            )
    )
  }

  public void testTheUseOfArgForRulesThatDoNotDependOnClosureScopeInputArgIsScalarAndArgsPassedByPredicateAreNotScalar() {
    assert "abc" == new Decision().withArgs("a").apply(
            new Rule({["b", "c"]}, {a, b, c -> a + b + c})
    )
  }

  public void testTheUseOfArgForRulesThatDoNotDependOnClosureScopeWhenBothInputAndArgsPassedByPredicateAreNotScalar() {
    assert "abcd" == new Decision().withArgs(["a", "b"]).apply(
            new Rule({["c", "d"]}, {a, b, c, d -> a + b + c + d})
    )

  }

  public void testSettingRulesRatherThanPassingThemInAsArguments() {
    Var var = new Var("x")
    def match = new Decision()
    match.rules << new Rule({var.name == "x"}, {5})
    assert 5 == match.apply()
  }

  //@Typed
  public void testAbiltyToPreventExceptionWhenNoMatch() {
    def match = new Decision()
    try {
      match.apply()
      assert false
    } catch (NoMatchException es) {}
    match.okToThrowExceptionWhenNoMatch(false)
    match.apply()
  }

  public void testDoNothingRule() {
    new Decision().apply(rule({false}, {}), DoNothingRule())
  }

  //@Typed
  public void testPredicatePassesAListRatherThanOneOrMoreScalarsToAction() {
    //An action needs to wrap a list inside a list in order to pass it to an action
    def expected = [1, 2, 3]
    assert expected == new Decision().apply(
            //right here
            new Rule({ [expected] }, {it})
    )
  }

  public void testPredicatePassesAListToActionAfterHavingCalledTheArgsMethod() {
    def originalList = [1, 2, 3]
    def argsForBothPredAndAction = ["hey", "ho"]
    assert (originalList + argsForBothPredAndAction) == new Decision().withArgs(argsForBothPredAndAction).apply(
            //Need to put list into list
            rule({ hey, ho -> def temp = originalList.clone(); temp << hey; temp << ho; [temp]},
                    {a, b, list -> list})
    )
  }

  //@Typed
  public void testChainingWithGroovypp() {
    assert 2 == new Decision().withArgs(1).apply(new Rule({ ((int) it) + 1}, { arg, passed -> passed}))
  }

  public void testIfTrue() {
    def expected = 1
    assert expected == toIf({true}, {expected}).call()
  }

  public void testIfFalseReturnsFalse() {
    assert !toIf({false}, {}).call()
  }

  public void testTreeVar() {
    assert 5 == eval(new Var("x"), makeEnv())
  }

  public void testTreeConstant() {
    def c = makeConstant()
    assert c.v == eval(c, makeEnv())
  }

  public void testTreeSumOfConstants() {
    def constant = makeConstant()
    def sum = new Sum(constant, constant)
    assert 2 * constant.v == eval(sum, makeEnv())
  }


  public void testrSimpleIfDoUsage() {
    Closure pred = {true}
    Closure action = {3}
    assert 3 == new Decision().if(pred).do(action).apply()
  }

  public void testRuleEval() {
    int expected = 5
    assert expected == rule({expected}, {it}).eval()
  }


}
//@Typed
class Student {
  def name
  def grade

}

//@Typed
class StudentRepository {
  List<Student> getStudentsForCourse(String course) {
    return null
  }
}

//@Typed
class StudentHandler {
  Decision rules
  def StudentRepository repository

  def handle(String course) {
    repository.getStudentsForCourse(course).each {  student ->
      rules.withArgs(student).apply()
    }
  }

}


class MultiMap extends HashMap {
    public Object put(Object key, Object value) {
        ArrayList oldList = (ArrayList) this.get(key);
        ArrayList newList = (oldList == null) ? new ArrayList() : new ArrayList(oldList);
        newList.add(value);
        super.put(key, newList);
        return oldList;
    }
}

class Company{
   def int profits
   def int revenue
   def String  name
}